package ee.siimoliver.chat.server;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Vector;

import ee.siimoliver.chat.controllers.ChatSessionController;
import ee.siimoliver.chat.utils.ApplicationProperties;
import ee.siimoliver.chat.utils.ChatUtils;
import ee.siimoliver.chat.utils.Const;
import ee.siimoliver.chat.utils.HttpConstants;
import ee.siimoliver.chat.utils.JsonParser;

public class Worker extends WebServer implements HttpConstants, Runnable {
    final WebServer webServer;
	
	final int BUF_SIZE = 2048;

    final byte[] EOL = {(byte)'\r', (byte)'\n' };

    /* buffer to use for requests */
    byte[] buf;
    /* Socket to client we're handling */
    private Socket socket;

    Worker(WebServer webServer) {
    	this.webServer = webServer;
        buf = new byte[BUF_SIZE];
        socket = null;
    }

    synchronized void setSocket(Socket s) {
        this.socket = s;
        notify();
    }
    
    synchronized void setSocketToNull() {
    	this.socket = null;
    	notify();
    }

    public synchronized void run() {
        while(webServer.isServerRunning()) {
            if (socket == null) {
                /* nothing to do */
                try {
                    wait();
                } catch (InterruptedException e) {
                    /* should not happen */
                    continue;
                }
            }
            try {
            	if (socket != null) {
            		handleClient();
            	}
            } catch (Exception e) {
                e.printStackTrace();
            }
            /* go back in wait queue if there's fewer
             * than numHandler connections.
             */
            socket = null;
            Vector<Worker> pool = webServer.threads;
            synchronized (pool) {
                if (pool.size() >= webServer.workers) {
                    /* too many threads, exit this one */
                    return;
                } else {
                    pool.addElement(this);
                }
            }
        }
    }
    
    private boolean readFromStreamToBuffer(InputStream is) throws IOException {
    	int r = 0;
    	int nread = 0;
        outerloop: while (nread < BUF_SIZE) {
            r = is.read(buf, nread, BUF_SIZE - nread);
            if (r == -1) {
                /* EOF */
                return false;
            }
            int i = nread;
            nread += r;
            for (; i < nread; i++) {
                if (buf[i] == (byte)'\n' || buf[i] == (byte)'\r') {
                    /* read one line */
                    break outerloop;
                }
            }
        }
		return true;
    }
    
    void handleClient() throws IOException {
    	System.out.println("Handling request");
    	InputStream is = new BufferedInputStream(socket.getInputStream());
    	PrintStream ps = new PrintStream(socket.getOutputStream());
    	socket.setSoTimeout(webServer.timeout);
    	socket.setTcpNoDelay(true);
    	/* zero out the buffer from last time */
    	for (int i = 0; i < BUF_SIZE; i++) {
    		buf[i] = 0;
    	}
    	try {
    		if (!readFromStreamToBuffer(is)) {
    			return;
    		}
    		String ipPort = socket.getInetAddress().getHostAddress() + Const.COLON + socket.getPort();
    		HttpMessage request = new HttpMessage(buf, ipPort);
    		switch (request.getType()) {
			case GET:
				System.out.println(request);
				if (request.getPath().equalsIgnoreCase(Const.METHOD_SENDMESSAGE)) {
					sendMessage(ps, request);
				} else if (request.getPath().equalsIgnoreCase(Const.METHOD_ASKNAMES)) {
					askNames(ps);
				} else if (request.getPath().equalsIgnoreCase(Const.METHOD_SENDNAME)) {
					printResponseHeaders(ps, Const.MIME_HTML, "0");
					InputBuffer.getInstance().addMessage(request);
				} else if (request.getPath().equalsIgnoreCase(Const.METHOD_FINDNAME)) {
					System.out.println("METHOD - find name");
					printResponseHeaders(ps, Const.MIME_HTML, "0");
					socket.close();
					findName(request);
				}
				break;
			case POST:
				System.out.println(request);
				
				break;
			default:
				/* we don't support this method */
				ps.print("HTTP/1.0 " + HTTP_BAD_METHOD +
						" unsupported method type: ");
				ps.write(buf, 0, 5);
				ps.write(EOL);
				ps.flush();
			}
    	} catch (Exception e) {
    		System.err.println(e.getMessage());
    		e.printStackTrace();
    		ps.print(Const.HEADER_HTTP_VER + HTTP_INTERNAL_ERROR +
    				" Internal server error");
            ps.write(EOL);
			ps.flush();
    	} finally {
    		System.out.println("socket closed");
    		if (!socket.isClosed()) {
    			socket.close();
    		}
    	}
    }
    
    private void sendMessage(PrintStream ps, HttpMessage request) throws IOException {
    	HashMap<String, String> requestParameters = request.getGETParameters();
    	String message = requestParameters.get(Const.HEADER_KEY_SENDMESSAGE);
    	ChatMate chatmate = new ChatMate(requestParameters.get(Const.HEADER_KEY_MYNAME), 
    			requestParameters.get(Const.HEADER_KEY_MYIP));
    	int sessionsNotified = 0;
    	ArrayList<ChatMate> chatmates = new ArrayList<ChatMate>();
    	boolean isMultichat = (requestParameters.containsKey(Const.HEADER_KEY_OTHER_MATES)) ? true : false;
    	if (isMultichat) {
    		chatmates = decodeOtherMates(requestParameters.get(Const.HEADER_KEY_OTHER_MATES));
    		for (ChatSessionController session : webServer.getChatSessions()) {
	    		if (session.hasChatmate(chatmate)) {
	    			addChatMatesToFile(chatmates);
	    			for (ChatMate cm : chatmates) {
	    				if (!session.hasChatmate(cm)) session.addChatmate(cm.getName(), cm.getIpPort());
	    			}
	    			session.getOutputController().appendText(ChatUtils.decodeString(message, IS_CUSTOM_CODING), chatmate.getName());
	    			sessionsNotified++;
	    			break;
	    		}
	    	}
    	} else {
    		for (ChatSessionController session : webServer.getChatSessions()) {
	    		if (session.hasChatmate(chatmate)) {
	    			session.getOutputController().appendText(ChatUtils.decodeString(message, IS_CUSTOM_CODING), chatmate.getName());
	    			sessionsNotified++;
	    		}
	    	}
    	}
    	if (sessionsNotified == 0 && !isMultichat) {
    		ChatSessionController chatSession = webServer.startNewChatSession(chatmate.getName(), chatmate.getIpPort());
    		chatSession.getOutputController().appendText(ChatUtils.decodeString(message, IS_CUSTOM_CODING), chatmate.getName());
    	} else if (sessionsNotified == 0 && isMultichat) {
    		chatmates.add(chatmate);
    		ChatSessionController chatSession = webServer.startNewChatSession(chatmates);
    		chatSession.getOutputController().appendText(ChatUtils.decodeString(message, IS_CUSTOM_CODING), chatmate.getName());
    	}
    	printResponseHeaders(ps, Const.MIME_HTML, "0");
    }
    
    private void addChatMatesToFile(ArrayList<ChatMate> chatmates) {
    	try {
			String tableFileName = String.format(USERS_TABLE_FILE, 
					ApplicationProperties.getInstance().getProperty(PROP_USERNAME));
			HashMap<String, String> usersTable = JsonParser.readFile(tableFileName);
			for (ChatMate cm : chatmates)	
					usersTable.put(cm.getName(), cm.getIpPort());
			JsonParser.writeFile(tableFileName, usersTable);
		} catch (IOException e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
		}
    }
    
    private void askNames(PrintStream ps) throws IOException {
    	System.out.println("opening file");
    	File responseFile = new File(String.format(Const.USERS_TABLE_FILE, 
				ApplicationProperties.getInstance().getProperty(Const.PROP_USERNAME)));
    	System.out.println("writing response headers");
    	printResponseHeaders(ps, Const.MIME_JSON, String.valueOf(responseFile.length()));
    	System.out.println("sending file");
    	sendFile(responseFile, ps);
    }
    
    private void findName(HttpMessage request) throws NumberFormatException, IOException {
    	System.out.println("Finding name " + request.getGETParameters().get(Const.HEADER_KEY_NAME));
    	HashMap<String, String> userTable = JsonParser.readFile(String.format(USERS_TABLE_FILE, 
				ApplicationProperties.getInstance().getProperty(PROP_USERNAME)));
    	HashMap<String, String> getParameters = request.getGETParameters();
    	if (userTable.containsKey(getParameters.get(Const.HEADER_KEY_NAME)) && !getParameters.get(Const.HEADER_KEY_TTL).equals("0")) {
    		String url = Const.HEADER_HTTP_PREFIX + getParameters.get(Const.HEADER_KEY_MYIP) + Const.METHOD_SENDNAME
			+ Const.QUESTION_MARK + Const.HEADER_KEY_NAME + Const.EQUALS + getParameters.get(Const.HEADER_KEY_NAME)
			+ Const.AMPERSAND + Const.HEADER_KEY_IP + Const.EQUALS
			+ userTable.get(getParameters.get(Const.HEADER_KEY_NAME))
			+ Const.AMPERSAND + Const.HEADER_KEY_TTL + Const.EQUALS
			+ (Integer.parseInt(getParameters.get(Const.HEADER_KEY_TTL)) - 1);
    		System.out.println(url);
    		ChatUtils.makeGETRequestPrintResponse(url);
    	} else {
    		if (getParameters.get(Const.HEADER_KEY_TTL).equals("0")) return;
    		for (String s : userTable.keySet()) {
    			if (userTable.get(s).equals(getParameters.get(Const.HEADER_KEY_MYIP))
    					|| userTable.get(s).equals(request.getIpPort())) continue;
    			try {
	    			ChatUtils.makeGETRequestPrintResponse(Const.HEADER_HTTP_PREFIX + userTable.get(s) + Const.METHOD_FINDNAME
	    				+ Const.QUESTION_MARK + Const.HEADER_KEY_NAME + Const.EQUALS + getParameters.get(Const.HEADER_KEY_NAME)
	    				+ Const.AMPERSAND + Const.HEADER_KEY_MYIP + Const.EQUALS
	    				+ getParameters.get(Const.HEADER_KEY_MYIP)
	    				+ Const.AMPERSAND + Const.HEADER_KEY_TTL + Const.EQUALS
	    				+ (Integer.parseInt(getParameters.get(Const.HEADER_KEY_TTL)) - 1));
    			} catch (Exception e) {
    				System.out.println(e.getMessage());
    				continue;
    			}
    		}
    	}
    }
    	
    
    private void printResponseHeaders(PrintStream ps, String contentType, String contentLength) throws IOException {
    	String[] headers = new String[] {
    			Const.HEADER_HTTP_VER + HTTP_OK + " OK",
    			Const.HEADER_SERVER,
    			"Date: " + (new Date()),
    			"Content-type: " + contentType,
    			"Content-length: " + contentLength
    	};
    	for (String header : headers) {
    		ps.print(header);
    		ps.write(EOL);
    	}
    	ps.write(EOL);
    }
    
    private void sendFile(File fileName, PrintStream ps) throws IOException {
		InputStream is = null;
		is = new FileInputStream(fileName);
		try {
			int n;
			while ((n = is.read(buf)) > 0) {
				ps.write(buf, 0, n);
			}
		} finally {
			is.close();
		}
	}
    
    private ArrayList<ChatMate> decodeOtherMates(String otherMates) {
    	ArrayList<ChatMate> result = new ArrayList<ChatMate>();
    	String[] mates = otherMates.split(";");
    	int index;
    	for (String s : mates) {
    		index = s.indexOf(',');
    		result.add(new ChatMate(s.substring(0, index), s.substring(index + 1)));
    	}
    	return result;
    }
}